上一篇提到,一個 Process 不只有程式碼。
當它正在 CPU 上執行時,還有一組很重要的 CPU Execution State,例如:
假設現在 CPU 正在執行 Process A:
CPU
│
▼
Process A
但系統裡還有:
Process B
Process C
Process D
...
OS 不可能永遠只讓 A 使用 CPU。
如果現在要把 CPU 從 A 換給 B,A 執行到一半的狀態怎麼辦?
這就是今天的主角:
Context Switch
假設我們只有一個 CPU Core。
在某一個瞬間,這個 Core 只能執行一個 instruction stream。
但我們平常使用電腦時,看起來可以同時:
播放音樂 + 開瀏覽器 + 使用 VS Code + 下載檔案 + 跑 Terminal
看起來好像全部都「同時在跑」。
其中一個重要原因,就是 OS 會讓 CPU 在不同可執行工作之間切換。
例如:
時間 ───────────────────────────►
Process A
████████
Process B
████████
Process C
████████
Process A
████████
當然,如果是 Multi-core CPU,不同 Core 本來就能真正同時執行不同的工作。
但即使有很多 Core,可以執行的 Process / Thread 數量通常仍然遠大於 CPU Core 數量。
所以 OS 還是需要在不同工作之間分配 CPU。
但是,怎麼讓 A 暫停之後,待會或等等可以從原本的位置繼續?
讓一個執行實體在未來能正確繼續執行所需要保存的 CPU 狀態。
假設 Process A 正在做:
int c = a + b;
到了某個時間點,CPU 內部可能存在:
Program Counter = 0x401020
Stack Pointer = 0x7fff....
Register R1 = 10
Register R2 = 20
Register R3 = ...
...
它們是:程式「現在執行到哪裡」的即時狀態。
如果 OS 直接讓 Process B 開始使用 CPU:
Process A
R1 = 10
R2 = 20
PC = A 的下一條指令
│
│ 不保存,直接換 B
▼
Process B
R1 = B 的資料
R2 = B 的資料
PC = B 的指令
那 A 原本存在 CPU Registers 裡的資訊就會被覆蓋。
等 A 下次回來,
A:???我剛才跑到哪?
所以 Context Switch 的第一個核心就是:
在 CPU 被另一個執行實體使用之前,先保存目前必要的 execution context。
假設:
Process A = Running
Process B = Ready
CPU 正在執行 A。
某個時刻,Kernel 決定接下來讓 B 執行。
process A
│
│ Running
▼
CPU
│
│ ① 進入 Kernel
▼
Kernel
│
│ ② 保存 A 的 Context
▼
A 的 Kernel Management Data
│
│
│ ③ Scheduler 決定 B 執行
▼
B 的 Kernel Management Data
│
│ ④ 恢復 B 的 Context
▼ CPU
│
│ ⑤ 返回 B 的執行流程
▼ Process B
① CPU 先進入 Kernel:Process A 不能自己任意決定:「好,我現在把 CPU 送給 Process B。」
CPU 排程與 Process 管理屬於 Kernel 的工作。
② 保存 A 的 Context:保存 A 未來恢復執行所需要的狀態,即使 CPU Registers 接下來被 B 使用,也不會讓 A 的必要狀態直接消失。
③ Scheduler 選擇下一個工作:接著 OS 的 Scheduler 會根據排程政策,從可以執行的工作中決定下一個要使用 CPU 的對象,例如:
④ 恢復 B 的 Context:假設 B 以前執行過,那 Kernel 會有它之前保存下來的 execution context,例如:
B:
PC = 0x...
SP = 0x...
Registers = ...
Kernel 將必要的狀態恢復到 CPU。
這時 CPU 的執行環境就從:
⑤ CPU 繼續執行 B:最後 CPU 從 B 對應的執行位置繼續,對 B 來說,它可能完全不會感覺到:我剛才被停了 10 ms,它看到的邏輯仍然:
Instruction 1
Instruction 2
Instruction 3
--- (沒看到的部分但中間其實被切走) ---
Instruction 4
Instruction 5
這就是 OS 能讓大量工作輪流使用 CPU,卻仍然維持各自執行狀態的重要基礎。
不是只有「時間到了」才會切換,常見原因可以分成幾種。
Process B:
Ready
↓
Running
這時可能發生 Context Switch。
還記得 Day 2 的:read(fd, buffer, 100);如果需要的資料還沒有準備好,目前執行的工作可能必須等待。
此時:
Process A:
Running
│
│ 等待 I/O
▼
Waiting
既然 A 現在不能繼續,讓 CPU 空等通常沒有意義。
Scheduler 就可以讓另一個 Ready 的工作執行:
A → Waiting
B → Running
這也可能造成 Context Switch。
CPU 為了加速執行,本身還有很多 cache,例如:
CPU
│
├── Registers
├── Cache
└── TLB
當 CPU 長時間執行 Process A 時,Cache 裡可能已經有很多 A 常用的資料與指令,如果現在突然切到 Process B,B 使用的資料可能完全不同,那 B 一開始就可能遇到更多 cache miss,需要重新把自己的 working set 帶進 cache。
所以 Context Switch 的成本不只有「保存和恢復 Registers + 花幾個指令。」,還可能造成 Cache Locality 變差。
如果完全不切:
Process A 那麼長
████████████████████████████████████████
那其他 Process 可能很久都拿不到 CPU。
→ 其他工作等待較久
→ Responsiveness / Fairness 可能變差
但如果切得太頻繁:
A B C A B C A B C A B C A B C...
又會花太多時間在切換本身。
→ Context Switch Overhead 增加
→ Cache / TLB 等副作用也可能增加
OS 還要決定:誰先跑?跑多久?什麼時候使用CPU?
這些就是後面的 Scheduling 問題。
CPU 要換人使用,但原本的工作之後還必須能從原來的位置繼續。
System Call
→ User Program 向 Kernel 請求服務
Interrupt
→ CPU 收到需要處理的事件
Context Switch
→ CPU 從一個 execution context 切換到另一個 execution context
對了,fork()是OS中很重要的角色,如果 fork() 要建立 Child Process,難道真的要把 Parent 的整份記憶體全部複製一次嗎?
下一篇: